<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 fdm=marker: */

/**
 * LightBot
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category  LightBot
 * @package   LightBot
 * @copyright Copyright (c) 2008 Olivier Hoareau <hoareau.olivier@gmail.com>
 * @author    Olivier Hoareau <hoareau.olivier@gmail.com>
 * @license   License http://framework.zend.com/license   New BSD License
 * @version   $Id:$
 */

require_once dirname(__FILE__) . '/ILightBot.php';

// {{{ LightBot_LightBot

/**
 * Light bot implementation
 *
 * @category  LightBot
 * @package   LightBot
 * @copyright Copyright (c) 2008 Olivier Hoareau <hoareau.olivier@gmail.com>
 * @author    Olivier Hoareau <hoareau.olivier@gmail.com>
 * @license   License http://framework.zend.com/license   New BSD License
 */
class LightBot_LightBot implements LightBot_ILightBot
{
    // {{{ properties

    /**
     * Flag to indicates whether the bot is marked for stopping or not
     * @var boolean
     */
    private $markStop;

    /**
     * Flag that indicates that the bo is running or not
     * @var boolean
     */
    private $running;

    /**
     * The event listener to notify on events
     * @var LightBot_IListener
     */
    private $listener;

    /**
     * Configuration properties
     * @var array
     */
    private $conf;

    /**
     * Protocol Adapter
     * @var LightBot_IProtocolAdapter
     */
    private $protocolAdapter;

    /**
     * Action Adapter
     * @var LightBot_IActionAdapter
     */
    private $actionAdapter;

    /**
     * Logger
     * @var LightBot_ILogger
     */
    private $logger;

    // }}}
    // {{{ __construct()

    /**
     * Constructs a new LightBot
     */
    public function __construct()
    {
        $this->running         = false;
        $this->conf            = null;
        $this->listener        = null;
        $this->protocolAdapter = null;
        $this->actionAdapter   = null;
        $this->logger          = null;
    }

    // }}}
    // {{{ configure()

    /**
     * Configures the Light bot with the specified configuration settings
     *
     * @param  mixed     $conf filename or array
     * @return LightBot  $this
     * @throws Exception if an error occured
     */
    public function configure($conf)
    {
        if($this->conf !== null) throw new Exception("Bot is already configured");

        if(is_string($conf)) {
            // file mode
            if(!array_key_exists('LIGHTBOT_ENV',$_SERVER)) throw new Exception("Environment variable 'LIGHTBOT_ENV' is not set");
            if(!file_exists($conf)) throw new Exception("Configuration file '$conf' does not exist are is not readable");
            $conf = @parse_ini_file($conf,true);
            if($conf===null || !is_array($conf) || count($conf)===0) throw new Exception("Configuration file '$conf' is empty of contains non valid data");
            if(!array_key_exists($_SERVER['LIGHTBOT_ENV'],$conf)) throw new Exception("Configuration file '$conf' does not contain the section '{$_SERVER['LIGHTBOT_ENV']}'");
            $this->conf = $conf[$_SERVER['LIGHTBOT_ENV']];
        }elseif(is_array($conf)){
            $this->conf = $conf;
        }else{
            throw new Exception("Configuration settings are not in valid format (filename or array expected)");
        }
        // logger
        if(array_key_exists('logger',$this->conf)) {
            // checks if logger is a named logger (no class name)
            // try to add prefix to see if class exists
            $class = "LightBot_Logger_".ucfirst($this->conf['logger']);
            $file = dirname(__FILE__).'/../'.str_replace("_","/",$class).".php";
            if(is_file($file)){
                // exists
                require_once($file);
            }else{
                $class = $this->conf['logger'];
                $file = str_replace("_","/",$class).".php";
                if(is_file($file)){
                    // exists
                    require_once($file);
                }else{
                    throw new Exception("Unable to load logger '{$this->conf['logger']}'");
                }
            }
            if(!class_exists($class)) {
                throw new Exception("Logger file '$file' does not contains valid logger class '$class'");
            }
            $this->setLogger(new $class);
        }
        // protocol
        if(array_key_exists('protocol',$this->conf)) {
            $this->loadProtocol($this->conf['protocol']);
        }
        // action adapter
        if(array_key_exists('action',$this->conf)) {
            $this->loadAction($this->conf['action']);
        }else{
            $this->loadAction('default');
        }
        // listener
        if(array_key_exists('listener',$this->conf)) {
            // checks if listener if a named listener (no class name)
            // try to add prefix to see if class exists
            $class = "LightBot_Listener_".ucfirst($this->conf['listener']);
            $file = dirname(__FILE__).'/../'.str_replace("_","/",$class).".php";
            if(is_file($file)){
                // exists
                require_once($file);
            }else{
                $class = $this->conf['listener'];
                $file = str_replace("_","/",$class).".php";
                if(is_file($file)){
                    // exists
                    require_once($file);
                }else{
                    throw new Exception("Unable to load listener '{$this->conf['listener']}'");
                }
            }
            if(!class_exists($class)) {
                throw new Exception("Listener file '$file' does not contains valid listener class '$class'");
            }
            $this->setListener(new $class);
        }
        return $this;
    }

    // }}}
    // {{{ getProperty()

    /**
     * Returns the value of the specified property
     *
     * @param string $name the name of the property
     * @param string $default the default value
     * @return string the key value
     */
    public function getProperty($name,$default=null)
    {
        return array_key_exists($name,$this->conf) ? $this->conf[$name] : $default;
    }

    // }}}
    // {{{ loadProtocol()

    protected function loadProtocol($protocolName)
    {
        $dir = dirname(__FILE__) . '/Protocol/'.ucfirst($protocolName);
        if(!is_dir($dir)) throw new Exception("Protocol '{$protocolName}' does not exist are is not installed");
        require_once $dir . '/ProtocolAdapter.php';
        $protocolClass = 'LightBot_Protocol_'.ucfirst($protocolName).'_ProtocolAdapter';
        $this->setProtocolAdapter(new $protocolClass);
        if(is_file($dir.'/SocketAdapter.php')){
            require_once $dir.'/SocketAdapter.php';
            $socketClass = 'LightBot_Protocol_'.ucfirst($protocolName).'_SocketAdapter';
        }else{
            require_once dirname(__FILE__).'/Protocol/Default/SocketAdapter.php';
            $socketClass = 'LightBot_Protocol_Default_SocketAdapter';
        }
        $this->getProtocolAdapter()->setSocketAdapter(new $socketClass);
    }

    // }}}
    // {{{ loadAction()

    protected function loadAction($actionName)
    {
        $file = dirname(__FILE__) . '/Action/'.ucfirst($actionName).'Adapter.php';
        if(!is_file($file)){
            $file = dirname(__FILE__) . '/Action/DefaultAdapter.php';
            if(!is_file($file)) throw new Exception("Neither '{$actionName}' nor default action adapter are installed");
            $actionClass = 'LightBot_Action_DefaultAdapter';
        }else{
            $actionClass = 'LightBot_Action_'.ucfirst($actionName).'Adapter';
        }
        require_once $file;
        $this->setActionAdapter(new $actionClass);
    }

    // }}}
    // {{{ setActionAdapter()

    /**
     * Sets the action adapter
     *
     * @param LightBot_IActionAdapter $actionAdapter the action adapter
     * @return LightBot_ILightBot $this
     */
    public function setActionAdapter(LightBot_IActionAdapter $actionAdapter)
    {
        $this->actionAdapter = $actionAdapter;
        $this->actionAdapter->configure($this->conf);
        return $this;
    }

    // }}}
    // {{{ getActionAdapter()

    /**
     * Returns the action adapter
     *
     * @return LightBot_IAction the action adapter
     */
    public function getActionAdapter()
    {
        return $this->actionAdapter;
    }

    // }}}
    // {{{ setProtocolAdapter()

    /**
     * Sets the underlying protocol adapter
     * 
     * @param  LightBot_IProtocolAdapter $protocolAdapter the protocol adpater
     * @return LightBot                $this
     */
    public function setProtocolAdapter(LightBot_IProtocolAdapter $protocolAdapter)
    {
        if($this->conf===null) throw new Exception("Configure the bot before setting the protocol adapter (use configure() )");
        $this->protocolAdapter = $protocolAdapter;
        $this->protocolAdapter->setLightBot($this);
        $this->protocolAdapter->configure($this->conf);
        return $this;
    }

    // }}}
    // {{{ getProtocolAdapter()

    /**
     * Returns the underlying protocol adapter
     *
     * @return LightBot_IProtocolAdapter the underlying protocol adapter
     */
    public function getProtocolAdapter()
    {
        return $this->protocolAdapter;
    }

    // }}}
    // {{{ setListener()

    /**
     * Sets the underlying event listener
     * 
     * @param  LightBot_IListener $listener the event listener
     * @return LightBot           $this
     */
    public function setListener(LightBot_IListener $listener)
    {
        $this->listener = $listener;
        $this->listener->setLightBot($this);
        return $this;
    }

    // }}}
    // {{{ getListener()

    /**
     * Returns the underlying event listener
     *
     * @return LightBot_IListener the underlying event listener
     */
    public function getListener()
    {
        return $this->listener;
    }

    // }}}
    // {{{ setLogger()

    /**
     * Sets the underlying logger
     * 
     * @param  LightBot_ILogger $logger the logger
     * @return LightBot           $this
     */
    public function setLogger(LightBot_ILogger $logger)
    {
        $this->logger = $logger;
        $this->logger->configure($this->conf);
        return $this;
    }

    // }}}
    // {{{ getLogger()

    /**
     * Returns the underlying logger
     *
     * @return LightBot_ILogger the underlying logger
     */
    public function getLogger()
    {
        return $this->logger;
    }

    // }}}
    // {{{ markStop()

    /**
     * Marks that the bot must stop as soon as possible
     *
     * @return LightBot $this
     */
    public function markStop()
    {
        $this->stopMarked = true;
    }

    // }}}
    // {{{ run()

    /**
     * Runs the bot (listen)
     *
     * @return boolean   always true
     * @throws Exception if connection or execution error occured
     */
    public function run()
    {
        if($this->running===true) throw new Exception("Bot already running");

        if($this->conf === null) throw new Exception("LightBot not configured, use method configure() before running");

        if($this->listener === null) throw new Exception("No event listener registered, use setListener() before running");

        if($this->protocolAdapter === null) throw new Exception("No Protocol defined for the bot, use setProtocolAdapter() before running");

        if($this->actionAdapter === null) throw new Exception("No Action Adapter defined for the bot, use setActionAdapter() before running");
        
        if($this->logger === null) throw new Exception("No logger registered, use setLogger() before running");

        $this->running = true;

        // connects/authenticates
        try {
            $socket = $this->protocolAdapter->connect();
        } catch(Exception $e) {
            try {
                $this->protocolAdapter->disconnect();
            } catch(Exception $e) {
            }
            throw new Exception("Unable to launch bot: ".$e->getMessage());
        }

        try {
            // errors counters
            $errors = array('receive' =>0,'ping'=>0,'incoming'=>0,'outgoing'=>0);

            // main loop
           while($this->running===true)
            {
                // read events
                try {
                    $this->protocolAdapter->receive();
                    $errors['receive'] = 0;
                }catch(Exception $e){
                    if($errors['receive']>0){
                        $this->logger->fatal("Multiple read error: ".$e->getMessage());
                        throw new Exception("Multiple read error: ".$e->getMessage());
                    }
                    $this->logger->error("Read error: ".$e->getMessage());
                    $errors['receive']++;
                }

                // send keep alive
                try {
                    $this->protocolAdapter->ping();
                }catch(Exception $e){
                    $this->logger->error("Ping error : ".$e->getMessage());
                }

                // process received data by notifying listener
                try {
                    $this->protocolAdapter->processIncoming();
                }catch(Exception $e){
                    $this->logger->error("Incoming error: ".$e->getMessage());
                }
                
                // send keep alive
                try {
                    $this->protocolAdapter->ping();
                }catch(Exception $e){
                    $this->logger->error("Ping error : ".$e->getMessage());
                }

                // executes registered outgoing events
                try {
                    $this->protocolAdapter->processOutgoing();
                }catch(Exception $e){
                    $this->logger->error("Outgoing error: ".$e->getMessage());
                }

                // send keep alive
                // send keep alive
                try {
                    $this->protocolAdapter->ping();
                }catch(Exception $e){
                    $this->logger->error("Ping error : ".$e->getMessage());
                }

                if($this->markStop===true) {
                    $this->running = false;
                }
            }
        } catch(Exception $e) {
            try {
                $this->protocolAdapter->disconnect();
            } catch(Exception $e) {
                $this->logger->fatal("Unable to disconnect cleanly the bot : ".$e->getMessage());
            }
            throw new Exception("Error when running bot: ".$e->getMessage());
        }

        return true;
    }

    // }}}
}

// }}}

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * c-hanging-comment-ender-p: nil
 * End:
 */
